feat: Implement event-sourced campaign status derivation with new sta… - #476
Conversation
|
@oyinade247 is attempting to deploy a commit to the ritik4ever's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughCampaign status now comes from replaying lifecycle events in event history, with cached derivation and cache invalidation on new writes. The store records updated lifecycle events for creation, funding, claim, and cancellation, and tests assert the replayed status flow. ChangesEvent-sourced campaign status
Estimated review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@oyinade247 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/index.ts`:
- Line 63: The status filter in listCampaigns is missing support for canceled,
so requests with ?status=canceled can fall through without applying the intended
predicate. Update normalizeStatusFilter and the listCampaigns status switch
together so canceled is handled explicitly, using the existing
CampaignStatus/CAMPAIGN_STATUSES flow and the same filtering logic as the other
statuses.
In `@backend/src/services/campaignStore.ts`:
- Line 10: The listCampaigns status filter in CampaignStore is missing support
for the new canceled CampaignStatus. Update the status switch to handle the
canceled branch explicitly alongside claimed, funded, failed, and open, and
ensure it does not fall through to the default deleted_at IS NULL behavior used
for active campaigns. Use the CampaignStatus and listCampaigns symbols to locate
the switch and add the canceled-specific filtering logic.
- Around line 154-175: The transitionCampaignToFailed logic is writing
campaign_failed for campaigns that were already canceled, and it can also emit
duplicate events when the database update is a no-op. Update
transitionCampaignToFailed to first guard against canceled campaigns and then
use the result of db.prepare(...).run(...) to only call recordEvent when the
failed_at update actually changed a row; use the existing CampaignRecord, getDb,
and recordEvent flow to keep the replay order stable.
In `@backend/src/services/eventHistory.ts`:
- Around line 5-15: `CampaignEventType` is missing an event literal still used
by `recordEvent`, so update the union in `eventHistory` to keep the existing
`'pledge_limit_reached'` value accepted alongside the current campaign event
types. Check the `CampaignEventType` definition and ensure any callers like
`recordEvent` in `campaignStore` continue to type-check without changing
existing event-writing behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 194f3419-c377-4321-a263-662b183d117e
📒 Files selected for processing (6)
backend/src/api.test.tsbackend/src/index.tsbackend/src/services/campaignStore.test.tsbackend/src/services/campaignStore.tsbackend/src/services/eventHistory.tsbackend/tests/integration.test.ts
| type CampaignListItem = CampaignRecord & { progress: CampaignProgress }; | ||
|
|
||
| const CAMPAIGN_STATUSES: CampaignStatus[] = ['open', 'funded', 'claimed', 'failed']; | ||
| const CAMPAIGN_STATUSES: CampaignStatus[] = ['open', 'funded', 'claimed', 'failed', 'canceled']; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Wire canceled through the list filter before accepting it.
normalizeStatusFilter now accepts canceled, but listCampaigns has no case 'canceled', so ?status=canceled falls through without a status predicate and can return the wrong campaigns.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/index.ts` at line 63, The status filter in listCampaigns is
missing support for canceled, so requests with ?status=canceled can fall through
without applying the intended predicate. Update normalizeStatusFilter and the
listCampaigns status switch together so canceled is handled explicitly, using
the existing CampaignStatus/CAMPAIGN_STATUSES flow and the same filtering logic
as the other statuses.
| } from './eventHistory'; | ||
|
|
||
| export type CampaignStatus = 'open' | 'funded' | 'claimed' | 'failed'; | ||
| export type CampaignStatus = CampaignLifecycleStatus; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle the new canceled status in listCampaigns.
CampaignStatus now includes canceled, but the status filter switch only handles claimed, funded, failed, and open. Add a canceled branch and avoid pairing it with the default deleted_at IS NULL filter.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/services/campaignStore.ts` at line 10, The listCampaigns status
filter in CampaignStore is missing support for the new canceled CampaignStatus.
Update the status switch to handle the canceled branch explicitly alongside
claimed, funded, failed, and open, and ensure it does not fall through to the
default deleted_at IS NULL behavior used for active campaigns. Use the
CampaignStatus and listCampaigns symbols to locate the switch and add the
canceled-specific filtering logic.
| function transitionCampaignToFailed(campaign: CampaignRecord, at: number): boolean { | ||
| if ( | ||
| campaign.claimedAt === undefined && | ||
| campaign.pledgedAmount < campaign.targetAmount && | ||
| at >= campaign.deadline && | ||
| campaign.failedAt === undefined | ||
| ) { | ||
| const db = getDb(); | ||
| db.prepare(`UPDATE campaigns SET failed_at = ? WHERE id = ? AND failed_at IS NULL`).run( | ||
| campaign.deadline, | ||
| campaign.id, | ||
| ); | ||
|
|
||
| recordEvent( | ||
| campaign.id, | ||
| 'campaign_failed', | ||
| campaign.deadline, | ||
| campaign.creator, | ||
| campaign.pledgedAmount, | ||
| { targetAmount: campaign.targetAmount, deadline: campaign.deadline }, | ||
| { source: 'local' } as BlockchainMetadata, | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Prevent canceled campaigns and failed no-ops from writing campaign_failed.
A campaign canceled before its deadline still satisfies this condition after the deadline. Recording campaign_failed at campaign.deadline makes replay overwrite the earlier campaign_canceled. Also, the conditional update result is ignored, so stale records can append duplicate failed events.
Proposed fix
function transitionCampaignToFailed(campaign: CampaignRecord, at: number): boolean {
if (
campaign.claimedAt === undefined &&
+ campaign.deletedAt === undefined &&
campaign.pledgedAmount < campaign.targetAmount &&
at >= campaign.deadline &&
campaign.failedAt === undefined
) {
const db = getDb();
- db.prepare(`UPDATE campaigns SET failed_at = ? WHERE id = ? AND failed_at IS NULL`).run(
+ const changes = db.prepare(`UPDATE campaigns SET failed_at = ? WHERE id = ? AND failed_at IS NULL`).run(
campaign.deadline,
campaign.id,
);
+ if (changes.changes === 0) {
+ return false;
+ }
recordEvent(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function transitionCampaignToFailed(campaign: CampaignRecord, at: number): boolean { | |
| if ( | |
| campaign.claimedAt === undefined && | |
| campaign.pledgedAmount < campaign.targetAmount && | |
| at >= campaign.deadline && | |
| campaign.failedAt === undefined | |
| ) { | |
| const db = getDb(); | |
| db.prepare(`UPDATE campaigns SET failed_at = ? WHERE id = ? AND failed_at IS NULL`).run( | |
| campaign.deadline, | |
| campaign.id, | |
| ); | |
| recordEvent( | |
| campaign.id, | |
| 'campaign_failed', | |
| campaign.deadline, | |
| campaign.creator, | |
| campaign.pledgedAmount, | |
| { targetAmount: campaign.targetAmount, deadline: campaign.deadline }, | |
| { source: 'local' } as BlockchainMetadata, | |
| ); | |
| function transitionCampaignToFailed(campaign: CampaignRecord, at: number): boolean { | |
| if ( | |
| campaign.claimedAt === undefined && | |
| campaign.deletedAt === undefined && | |
| campaign.pledgedAmount < campaign.targetAmount && | |
| at >= campaign.deadline && | |
| campaign.failedAt === undefined | |
| ) { | |
| const db = getDb(); | |
| const changes = db.prepare(`UPDATE campaigns SET failed_at = ? WHERE id = ? AND failed_at IS NULL`).run( | |
| campaign.deadline, | |
| campaign.id, | |
| ); | |
| if (changes.changes === 0) { | |
| return false; | |
| } | |
| recordEvent( | |
| campaign.id, | |
| 'campaign_failed', | |
| campaign.deadline, | |
| campaign.creator, | |
| campaign.pledgedAmount, | |
| { targetAmount: campaign.targetAmount, deadline: campaign.deadline }, | |
| { source: 'local' } as BlockchainMetadata, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/services/campaignStore.ts` around lines 154 - 175, The
transitionCampaignToFailed logic is writing campaign_failed for campaigns that
were already canceled, and it can also emit duplicate events when the database
update is a no-op. Update transitionCampaignToFailed to first guard against
canceled campaigns and then use the result of db.prepare(...).run(...) to only
call recordEvent when the failed_at update actually changed a row; use the
existing CampaignRecord, getDb, and recordEvent flow to keep the replay order
stable.
| export type CampaignEventType = | ||
| | 'created' | ||
| | 'pledged' | ||
| | 'claimed' | ||
| | 'refunded' | ||
| | 'updated' | ||
| | 'campaign_opened' | ||
| | 'campaign_funded' | ||
| | 'campaign_claimed' | ||
| | 'campaign_failed' | ||
| | 'campaign_canceled'; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Keep existing event literals in CampaignEventType.
recordEvent is still called with 'pledge_limit_reached' in backend/src/services/campaignStore.ts Line 816, but this union no longer accepts it. That will break TypeScript checking for the existing event write.
Proposed fix
export type CampaignEventType =
| 'created'
| 'pledged'
| 'claimed'
| 'refunded'
| 'updated'
+ | 'pledge_limit_reached'
| 'campaign_opened'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export type CampaignEventType = | |
| | 'created' | |
| | 'pledged' | |
| | 'claimed' | |
| | 'refunded' | |
| | 'updated' | |
| | 'campaign_opened' | |
| | 'campaign_funded' | |
| | 'campaign_claimed' | |
| | 'campaign_failed' | |
| | 'campaign_canceled'; | |
| export type CampaignEventType = | |
| | 'created' | |
| | 'pledged' | |
| | 'claimed' | |
| | 'refunded' | |
| | 'updated' | |
| | 'pledge_limit_reached' | |
| | 'campaign_opened' | |
| | 'campaign_funded' | |
| | 'campaign_claimed' | |
| | 'campaign_failed' | |
| | 'campaign_canceled'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/services/eventHistory.ts` around lines 5 - 15,
`CampaignEventType` is missing an event literal still used by `recordEvent`, so
update the union in `eventHistory` to keep the existing `'pledge_limit_reached'`
value accepted alongside the current campaign event types. Check the
`CampaignEventType` definition and ensure any callers like `recordEvent` in
`campaignStore` continue to type-check without changing existing event-writing
behavior.
Closes #212
✅ Event-sourced campaign status is now implemented
The backend now derives campaign status by replaying explicit lifecycle transition events instead of relying only on the current clock and stored totals.
What changed
Verification
Made changes.
Summary by CodeRabbit
New Features
Bug Fixes